iOS调试 - 单元测试

每个 iOS 程序员都要时不时的为他们的 APP 做 Debug。除非你是那种超级大牛,否则你肯定体验过查了无数个小时的 Bug 最后才发现那仅仅是个简单的语法错误时那种油然而生的绝望感。或者更糟:你根本就没发现那些 Bug。无论你是编程新手,还是开发过很多 APP 的老司机,例行的写写单元测试会让你的代码更可靠,更安全,更容易 Debug!

你很走运,Xcode 7 和 Swift 支持单元测试。尽管单元测试不保证(有了它你就会写出)绝对没有 Bug 的 APP,它还是一种能让你验证每段代码是否如期工作,并让 debug 过程更加便利。

正如其名,在单元测试中你要为某段代码单元创建一些小规模的、针对其某个特性的测试,然后确保每个代码单元都能通过这些测试。如果通过的话,它的旁边会出现一个绿色小标志,而如果因故测试不通过, Xcode 会把该测试标记为 “failed”。这就提示你去查看代码,找出失败原因。

这篇教程将简要介绍如何在 Swift 项目中使用XCTest.framework进行代码单元测试。那么我们为什么需要做单元测试呢?单元测试对于我们有以下几点帮助:

  • 帮助理解需求,单元测试应该反映 Use Case,把被测单元当成黑盒测试其外部行为。

  • 提高实现质量,单元测试不保证程序做正确的事,但能帮助保证程序正确地做事,从而提高实现质量。

  • 测试成本低,相比集成测试、验收测试,单元测试所依赖的外部环境少,自动化程度高,时间短,节约了测试成本。

  • 反馈速度快,单元测试提供快速反馈,把 Bug 消灭在开发阶段,减少问题流到集成测试、验收测试和用户,降低了软件质量控制的成本。

  • 利于重构,由于有单元测试作为回归测试用例,有助于预防在重构过程中引入 Bug。

  • 文档作用,单元测试提供了被测单元的使用场景,起到了使用文档的作用。

  • 对设计的反馈,一个模块很难进行单元测试通常是不良设计的信号,单元测试可以反过来指导设计出高内聚、低耦合的模块。

默认测试类

首先我们新建一个项目SwiftUnitTest,它将在SwiftUnitTestTests目录下自动创建出一个默认测试类(文件)SwiftUnitTestTests.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import XCTest
@testable import SwiftUnitTest
class SwiftUnitTestTests: XCTestCase {
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
//在此可以定义测试中用到的属性
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
//在此添加准备代码。此方法在每个测试用例执行前执行
super.tearDown()
}
func testExample() {
// This is an example of a functional test case.
//这是一个测试用例
// Use XCTAssert and related functions to verify your tests produce the correct results.
//使用XCTAssert和相关方法测试结果是否正确
}
func testPerformanceExample() {
// This is an example of a performance test case.
//这是一个测试用例
self.measure {
// Put the code you want to measure the time of here.
//在此填写需要测量运行时间的代码
}
}
}

在这个文件中定义了一个测试类SwiftUnitTestTests,它里面包含了一个setUp()方法和tearDown()方法,分别用来在每个测试方法运行之前做初始化准备,和在测试方法运行之后做清理工作。此外,它还包含了以test开头命名的2个测试方法:testExample()testPerformanceExample()

我们需要注意:

  • 任何以test开头命名的的方法都是一个测试方法,在每次单元测试执行时自动执行,它没有返回值;
  • 在测试方法中,可以使用self.measureBlock() { }来测量代码的运行时间;
  • 测试方法执行的顺序跟测试方法名有关,比如test01()会优先于test02()执行

通过快捷键CMD+U即可运行当前的单元测试。可以看到所有测试方法已通过。同样,使用CMD+SHIFT+Y打开Console也能看到相应测试方法的运行提示。

定制测试类

为了更好的管理测试用例,我们建议为某个需要测试的类单独创建一个测试类(文件),在这之前,我们先创建一个简单的类用来测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person: NSObject {
var name: String!
override init() {
name = "Children"
}
func transfrom() {
name = "GrownMan"
}
}

这个类定义了一种Person类型,它有一个属性叫name,其初始状态为Children。我们可以对其调用transform方法,之后它就变成了GrownMan。

然后我们新建一个针对Person类做测试的测试类:新建文件,选择Test Case Class, 为此类取名为PersonTest(建议使用<待测试类名>Tests的形式):

然后我们删除原来的testExample测试方法,并新建方法:

1
2
3
4
5
func testPersonTransformation() {
let person = Person()
person.transfrom()
XCTAssert(person.name == "GrownMan", "Test result is correct")
}

注意:如果此时Xcode提示无法找到Person的类定义,请在顶部加上 @testable import SwiftUnitTest。里面的SwiftUnitTest是当前项目的名称。

在此测试方法中,我们创建出了一个新的Person实例,然后对其调用transform()方法。这时我们使用断言XCTAssert来判断person目前的name属性值是否为GrownManXCTAssert是一个全局函数,它的第一个参数为布尔表达式,如果为true表示断言通过;它的第二个参数为断言的描述。

注意:如果实例创建比较复杂,并且需要在多个测试方法中使用。你可以在类中定义对应的实例属性,并且在setUp()方法中进行初始化,在tearDown()方法中进行资源清理。

运行单元测试

此时执行CMD+U运行单元测试,可以看到单元测试都已通过: test

然后我们再修改原有测试方法testPerformanceExample()中的内容:

1
2
3
4
5
6
7
8
func testPerformanceExample() {
self.measure {
var sum: Double = 0
for i in 0...1000000 {
sum += Double(i)
}
}
}

再次CMD+U执行单元测试,等待几秒钟,可以看到所有测试用例都已通过,打开console可以看到testPerformanceExample()中代码的运行时间:

1
2
3
4
5
6
7
8
9
10
11
Test Case '-[SwiftUnitTestUITests.SwiftUnitTestUITests testExample]' started.
t = 0.00s Start Test at 2016-09-26 15:05:54.905
t = 0.00s Set Up
t = 0.01s Launch space.tianziyao.SwiftUnitTest
t = 3.99s Waiting for accessibility to load
t = 5.63s Wait for app to idle
t = 6.85s Tear Down
Test Case '-[SwiftUnitTestUITests.SwiftUnitTestUITests testExample]' passed (7.061 seconds).
Test Suite 'SwiftUnitTestUITests' passed at 2016-09-26 15:06:01.967.Executed 1 test, with 0 failures (0 unexpected) in 7.061 (7.063) seconds
Test Suite 'SwiftUnitTestUITests.xctest' passed at 2016-09-26 15:06:01.968.Executed 1 test, with 0 failures (0 unexpected) in 7.061 (7.065) seconds
Test Suite 'All tests' passed at 2016-09-26 15:06:01.969.Executed 1 test, with 0 failures (0 unexpected) in 7.061 (7.067) seconds

断言测试API列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
XCTFail(format…)
//生成一个失败的测试;
XCTAssertNil(a1, format...)
//为空判断,a1为空时通过,反之不通过;
XCTAssertNotNil(a1, format…)
//不为空判断,a1不为空时通过,反之不通过;
XCTAssert(expression, format...)
//当expression求值为TRUE时通过;
XCTAssertTrue(expression, format...)
//当expression求值为TRUE时通过;
XCTAssertFalse(expression, format...)
//当expression求值为False时通过;
XCTAssertEqualObjects(a1, a2, format...)
//判断相等,[a1 isEqual:a2]值为TRUE时通过,其中一个不为空时,不通过;
XCTAssertNotEqualObjects(a1, a2, format...)
//判断不等,[a1 isEqual:a2]值为False时通过;
XCTAssertEqual(a1, a2, format...)
//判断相等(当a1和a2是 C语言标量、结构体或联合体时使用,实际测试发现NSString也可以);
XCTAssertNotEqual(a1, a2, format...)
//判断不等(当a1和a2是 C语言标量、结构体或联合体时使用);
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)
//判断相等,(double或float类型)提供一个误差范围,当在误差范围(+/-accuracy)以内相等时通过测试;
XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...)
//判断不等,(double或float类型)提供一个误差范围,当在误差范围以内不等时通过测试;
XCTAssertThrows(expression, format...)
//异常测试,当expression发生异常时通过;反之不通过;(很变态)
XCTAssertThrowsSpecific(expression, specificException, format...)
//异常测试,当expression发生specificException异常时通过;反之发生其他异常或不发生异常均不通过;
XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)
//异常测试,当expression发生具体异常、具体异常名称的异常时通过测试,反之不通过;
XCTAssertNoThrow(expression, format…)
//异常测试,当expression没有发生异常时通过测试;
XCTAssertNoThrowSpecific(expression, specificException, format...)
//异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过;
XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)
//异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过;

Demo下载请点击这里

原文链接

http://letsswift.com/2014/06/swift-unit-test/